/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package com.urbanski.ksp.kspmultitool.util;

import com.csvreader.CsvReader;
import com.urbanski.ksp.kspmultitool.ui.Main;
import com.urbanski.ksp.kspmultitool.vo.Body;
import com.urbanski.ksp.kspmultitool.vo.Function;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.math.RoundingMode;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Stack;
import javax.swing.JFileChooser;
import javax.swing.JOptionPane;
import javax.swing.filechooser.FileNameExtensionFilter;

/**
 *
 * @author murbanski004
 */
public class Utils
{
    public static DecimalFormat DF_ROUND_3 = new DecimalFormat("#.000");
    public static DecimalFormat DF_NO_DECIMAL_3 = new DecimalFormat("#,###.###");
    public static DecimalFormat DF_SCIENTIFIC_3 = new DecimalFormat("0.000E0");

    static
    {
        DF_ROUND_3.setRoundingMode(RoundingMode.HALF_UP);
        DF_NO_DECIMAL_3.setRoundingMode(RoundingMode.HALF_UP);
    }

    public static Map<String, Body> readBodiesFile(String filename) throws IOException
    {
        Map<String, Body> bodies = new LinkedHashMap<String, Body>(17);

        CsvReader reader = new CsvReader(new FileReader(filename));
        reader.setComment('#');
        reader.readHeaders();

        while (reader.readRecord())
        {
            String name = reader.get("Name");
            String parentName = reader.get("Parent");
            Body parent = bodies.get(parentName);
            double sma = Double.parseDouble(reader.get("SMA(m)"));
            double e = Double.parseDouble(reader.get("Eccentricity"));
            double inc = Double.parseDouble(reader.get("Inclination"));
            double M = Double.parseDouble(reader.get("Mass(kg)"));
            int r = Integer.parseInt(reader.get("Radius(m)"));
            double soa = Double.parseDouble(reader.get("SoI(km)"));
            int maxElevation = Integer.parseInt(reader.get("Max elevation(m)"));
            int flyingLow = Integer.parseInt(reader.get("Flying low(km)"));
            double atmosphereHeight = Double.parseDouble(reader.get("Flying high(km)"));
            int nearSpace = Integer.parseInt(reader.get("Near space(km)"));

            Body b = new Body(name, parent, sma, e, inc, M, r, soa, maxElevation, flyingLow, atmosphereHeight, nearSpace);
            bodies.put(name, b);

//            b.printDetails();
        }

        return bodies;
    }

    public static String formatDistanceForLabel(double d)
    {
        if (d == 0)
        {
            return "-";
        }

        String[] units =
        {
            "m", "km", "Mm", "Gm"
        };
        int unitIndex = 0;
        double r = d;
        while (r > 1000 && unitIndex < 3)
        {
            r /= 1000;
            unitIndex++;
        }

        String unit = units[unitIndex];

        return DF_ROUND_3.format(r) + " " + unit + " (" + DF_NO_DECIMAL_3.format(d) + " m)";
    }

    public static String formatTimeForLabelKerbin(double t)
    {
        if (t == 0)
        {
            return "-";
        }
        long y = 0, d = 0, h = 0, m = 0, s = 0;

        long tt = Math.round(t);
        long t1 = tt;

        s = t1 % 60;
        t1 /= 60;
        m = t1 % 60;
        t1 /= 60;
        h = t1 % 6;
        t1 /= 6;
        d = t1;

        StringBuilder sb = new StringBuilder();

        if (d > 0)
        {
            sb.append(d).append(" d, ");
        }
        if (h > 0 || d > 0)
        {
            sb.append(h).append(" h, ");
        }
        if (m > 0 || h > 0 || d > 0)
        {
            sb.append(m).append(" m, ");
        }

        sb.append(s).append(" s");

        if (m > 0 || h > 0 || d > 0)
        {
            sb.append(" (").append(tt).append(" s)");
        }

        return sb.toString();
    }

    public static String formatTimeForLabelEarth(double t)
    {
        if (t == 0)
        {
            return "-";
        }
        long d = 0, h = 0, m = 0, s = 0;

        long tt = Math.round(t);
        long t1 = tt;

        s = t1 % 60;
        t1 /= 60;
        m = t1 % 60;
        t1 /= 60;
        h = t1 % 24;
        t1 /= 24;
        d = t1;

        StringBuilder sb = new StringBuilder();

        if (d > 0)
        {
            sb.append(d).append(" d, ");
        }
        if (h > 0 || d > 0)
        {
            sb.append(h).append(" h, ");
        }
        if (m > 0 || h > 0 || d > 0)
        {
            sb.append(m).append(" m, ");
        }

        sb.append(s).append(" s");

        if (m > 0 || h > 0 || d > 0)
        {
            sb.append(" (").append(tt).append(" s)");
        }

        return sb.toString();
    }

    public static String formatGravitationalParameter(double d)
    {
        String s = DF_SCIENTIFIC_3.format(d);
        String[] parts = s.split("E");
        return "<html>" + parts[0] + " x10<sup>" + parts[1] + "</sup> m<sup>3</sup>/s<sup>2</sup></html>";
    }

    public static String formatMass(double d)
    {
        String s = DF_SCIENTIFIC_3.format(d);
        String[] parts = s.split("E");
        return "<html>" + parts[0] + " x10<sup>" + parts[1] + "</sup> kg</html>";
    }

    public static String formatScienceAltitude(double d1, double d2)
    {
        if (d1 == 0 && d2 == 0)
        {
            return "-";
        }
        return DF_NO_DECIMAL_3.format(d1) + " km - " + DF_NO_DECIMAL_3.format(d2) + " km";
    }

    public static void savePreferences()
    {
    }

    public static void readPreferences()
    {
        File f = new File("preferences.txt");
        if (!f.exists())
        {
            return;
        }
    }

    public static double computeExpression(String expression)
    {
        int equalsIndex;
        String variableToAssign = null;
        if ((equalsIndex = expression.indexOf('=')) != -1)
        {
            variableToAssign = expression.substring(0, equalsIndex).trim();
            if (variableToAssign.isEmpty())
                throw new IllegalArgumentException("= found without an assignment variable");
            expression = expression.substring(equalsIndex + 1).trim();
            
        }
        
        if (expression.isEmpty())
                throw new IllegalArgumentException("Empty expression");
        
        if (variableToAssign == null)
        {
            char first = expression.charAt(0);
            if (first == '+' || first == '*' || first == '/' || first == '%' || first == '^')
                expression = "ans " + expression;
        }
        
        List<String> tokens = tokenizeExpression(expression);
        List<String> postfixTokens = convertToPostfix(tokens);
        
        Stack<Double> operands = new Stack<>();
        
        for (String token : postfixTokens)
        {
            if (!isFunction(token) && !isOperator(token))
                operands.push(getValue(token));
            else
                operands.push(evaluateOperator(token, operands));
        }
        
        double value = operands.pop();
        
        if (variableToAssign != null)
            Main.instance.userVariables.put(variableToAssign, value);
        
        Main.instance.userVariables.put("ans", value);
        
        return value;
    }
    private static final String OPERATORS = ",()^*/%+-";

    public static List<String> tokenizeExpression(String expression)
    {
        boolean readingVariable = false;
        boolean readingNumber = false;

        List<String> tokens = new ArrayList<>();
        StringBuilder currentToken = new StringBuilder();

        for (int i = 0; i < expression.length(); i++)
        {
            char c = expression.charAt(i);

            if (readingVariable)
            {
                if (isLetterOrNumber(c))
                {
                    currentToken.append(c);
                }
                else
                {
                    tokens.add(currentToken.toString());
                    currentToken.delete(0, currentToken.length());
                    readingVariable = false;
                    if (isOperator(c))
                    {
                        tokens.add(Character.toString(c));
                    }
                    else if (c != ' ')
                    {
                        throw new IllegalArgumentException("Syntax error - invalid token: " + c);
                    }
                }
            }
            else if (readingNumber)
            {
                if (isNumber(c) || c == '.')
                {
                    currentToken.append(c);
                }
                else if (isLetter(c))
                {
                    throw new IllegalArgumentException("Syntax error - encountered letter while parsing number (" + currentToken + ")");
                }
                else
                {
                    tokens.add(currentToken.toString());
                    currentToken.delete(0, currentToken.length());
                    readingNumber = false;
                    if (isOperator(c))
                    {
                        tokens.add(Character.toString(c));
                    }
                    else if (c != ' ')
                    {
                        throw new IllegalArgumentException("Syntax error - invalid token: " + c);
                    }
                }
            }
            else
            {
                if (c == ' ')
                {
                    continue;
                }
                else if (isOperator(c))
                {
                    tokens.add(Character.toString(c));
                }
                else if (isLetter(c))
                {
                    readingVariable = true;
                    currentToken.append(c);
                }
                else if (isNumber(c) || c == '.')
                {
                    readingNumber = true;
                    currentToken.append(c);
                }
                else
                {
                    throw new IllegalArgumentException("Syntax error - invalid token: " + c);
                }
            }

        }

        if (currentToken.length() > 0)
        {
            tokens.add(currentToken.toString());
        }

        return tokens;
    }

    public static List<String> convertToPostfix(List<String> infix)
    {
        List<String> postfix = new ArrayList<>(infix.size());
        Stack<String> stack = new Stack<>();

        for (String token : infix)
        {
            if (token.equals("("))
            {
                stack.push(token);
            }
            else if (token.equals(")"))
            {
                while (!stack.isEmpty() && !stack.peek().equals("("))
                {
                    postfix.add(stack.pop());
                }
                if (stack.isEmpty())
                {
                    throw new IllegalArgumentException("Unmatched parentheses encountered");
                }
                stack.pop(); //pop left-paren
                if (!stack.isEmpty() && isFunction(stack.peek()))
                {
                    postfix.add(stack.pop());
                }
            }
            else if (token.equals(","))
            {
                while (!stack.peek().equals("("))
                {
                    postfix.add(stack.pop());
                    if (stack.isEmpty())
                    {
                        throw new IllegalArgumentException("Unmatched parentheses encountered");
                    }
                }
            }
            else if (isOperator(token))
            {
                String stackTop;
                while (!stack.isEmpty() && isOperator(stackTop = stack.peek()))
                {
                    int tokenPrecedence = getPrecedence(token.charAt(0));
                    int topPrecedence = getPrecedence(stackTop.charAt(0));
                    if ((!isRightAssociative(token) && tokenPrecedence <= topPrecedence) || tokenPrecedence < topPrecedence)
                    {
                        postfix.add(stackTop);
                        stack.pop();
                    }
                    else
                    {
                        break;
                    }
                }
                stack.push(token);
            }
            else if (isFunction(token))
            {
                stack.push(token);
            }
            else
            {
                postfix.add(token);
            }
        }

        while (!stack.isEmpty())
        {
            if (stack.peek().equals("("))
            {
                throw new IllegalArgumentException("Unmatched parentheses encountered");
            }
            postfix.add(stack.pop());
        }

        return postfix;
    }

    public static boolean isLetter(char c)
    {
        return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_';
    }

    public static boolean isNumber(char c)
    {
        return c >= '0' && c <= '9';
    }

    public static boolean isLetterOrNumber(char c)
    {
        return isNumber(c) || isLetter(c);
    }

    public static boolean isVariableOrFunction(String s)
    {
        if (!isLetter(s.charAt(0)))
        {
            return false;
        }
        for (int i = 1; i < s.length(); i++)
        {
            if (!isLetterOrNumber(s.charAt(i)))
            {
                return false;
            }
        }
        return true;
    }

    public static boolean isNumber(String s)
    {
        for (int i = 0; i < s.length(); i++)
        {
            if (!isNumber(s.charAt(i)))
            {
                return false;
            }
        }
        return true;
    }

    public static boolean isOperator(char c)
    {
        return OPERATORS.indexOf(c) >= 0;
    }

    public static boolean isOperator(String s)
    {
        return getPrecedence(s.charAt(0)) >= 0;
    }

    public static boolean isFunction(String s)
    {
        for (Function f : Constants.FUNCTIONS)
            if (f.name.equals(s))
                return true;
        return false;
    }

    public static boolean isRightAssociative(String s)
    {
        return s.equals("^");
    }

    public static int getPrecedence(char c)
    {
        if (c == '^')
        {
            return 3;
        }
        else if (c == '*' || c == '/' || c == '%')
        {
            return 2;
        }
        else if (c == '+' || c == '-')
        {
            return 1;
        }
        else if (c == '=')
        {
            return 0;
        }
        else
        {
            return Integer.MIN_VALUE;
        }
    }

    public static double evaluateOperator(String operator, Stack<Double> stack)
    {
        switch (operator)
        {
            case "+":
                double a = stack.pop();
                double b = stack.pop();
                return b + a;
            case "-":
                a = stack.pop();
                if (stack.isEmpty()) return -a;
                b = stack.pop();
                return b - a;
            case "*":
                a = stack.pop();
                b = stack.pop();
                return b * a;
            case "/":
                a = stack.pop();
                b = stack.pop();
                return b / a;
            case "%":
                a = stack.pop();
                b = stack.pop();
                return b % a;
            case "^":
                a = stack.pop();
                b = stack.pop();
                return Math.pow(b, a);
            case "sqrt":
                a = stack.pop();
                return Math.sqrt(a);
            case "cbrt":
                a = stack.pop();
                return Math.cbrt(a);
            case "sin":
                a = stack.pop();
                return Math.sin(a);
            case "cos":
                a = stack.pop();
                return Math.cos(a);
            case "tan":
                a = stack.pop();
                return Math.tan(a);
            case "asin":
                a = stack.pop();
                return Math.asin(a);
            case "acos":
                a = stack.pop();
                return Math.acos(a);
            case "atan":
                a = stack.pop();
                return Math.atan(a);
            case "sinh":
                a = stack.pop();
                return Math.sinh(a);
            case "cosh":
                a = stack.pop();
                return Math.cosh(a);
            case "tanh":
                a = stack.pop();
                return Math.tanh(a);
            case "abs":
                a = stack.pop();
                return Math.abs(a);
            case "ceil":
                a = stack.pop();
                return Math.ceil(a);
            case "floor":
                a = stack.pop();
                return Math.floor(a);
            case "log":
                a = stack.pop();
                b = stack.pop();
                return Math.log(a) / Math.log(b);
            case "log10":
                a = stack.pop();
                return Math.log10(a);
            case "ln":
                a = stack.pop();
                return Math.log(a);
            case "max":
                a = stack.pop();
                b = stack.pop();
                return Math.max(a, b);
            case "min":
                a = stack.pop();
                b = stack.pop();
                return Math.min(a, b);
            case "round":
                a = stack.pop();
                return Math.round(a);
            case "roundto":
                a = stack.pop();
                b = stack.pop();
                a = Math.pow(10, a);
                return Math.round(a * b) / a;
            case "random":
                return Math.random();
            case "orbT":
                a = stack.pop(); //mu
                b = stack.pop(); //SMA
                return 2 * Math.PI * Math.sqrt(b * b * b / a);
        }
        
        /*
         * "sqrt", "cbrt", "sin", "cos", "tan", "asin", "acos",
        "atan", "sinh", "cosh", "tanh", "abs", "ceil", "floor",
        "log", "log10", "ln", "max", "min", "round", "random"
         */
        return 0;
    }
    
    public static double getValue(String s)
    {
        if (Main.instance.bodyVariables.containsKey(s))
            return Main.instance.bodyVariables.get(s);
        else if (Main.instance.userVariables.containsKey(s))
            return Main.instance.userVariables.get(s);
        else
        {
            try
            {
                return Double.parseDouble(s);
            }
            catch (NumberFormatException e)
            {
                throw new IllegalArgumentException("Unknown variable or invalid value: " + s);
            }
        }
    }
    
    public static JFileChooser getLoadFileDialog()
    {
        JFileChooser jfc = new JFileChooser(System.getProperty("user.dir"));
        jfc.setDialogTitle("Export variables");
        jfc.setDialogType(JFileChooser.OPEN_DIALOG);
        jfc.addChoosableFileFilter(new FileNameExtensionFilter("Text files", "txt", "TXT"));
        return jfc;
    }

    public static File getSaveFile()
    {
        while (true)
        {
            JFileChooser jfc = getSaveFileDialog();
            if (jfc.showSaveDialog(Main.instance) == JFileChooser.APPROVE_OPTION)
            {
                File f = jfc.getSelectedFile();
                if (f != null)
                {
                    if (!f.exists() && !f.getName().contains("."))
                    {
                        f = new File(f.getAbsolutePath() + ".icf");
                    }
                    if (f.exists())
                    {
                        if (showConfirmDialog(f.getName() + " exists. Overwrite?"))
                        {
                            return f;
                        }
                    }
                    else
                    {
                        return f;
                    }
                }
                else
                {
                    return null;
                }
            }
            else
            {
                return null;
            }
        }
    }

    public static JFileChooser getSaveFileDialog()
    {
        JFileChooser jfc = new JFileChooser(System.getProperty("user.dir"));
        jfc.setDialogTitle("Import variables");
        jfc.setDialogType(JFileChooser.SAVE_DIALOG);
        jfc.addChoosableFileFilter(new FileNameExtensionFilter("Text files", "txt", "TXT"));
        return jfc;
    }
    
    public static boolean showConfirmDialog(String message)
    {
        return JOptionPane.showConfirmDialog(Main.instance, message, "", JOptionPane.OK_CANCEL_OPTION) == JOptionPane.OK_OPTION;
    }

    public static void showMessageDialog(String message)
    {
        JOptionPane.showMessageDialog(Main.instance, message);
    }

    public static void showErrorDialog(String message, Exception e)
    {
        if (e != null && e.getMessage() != null)
        {
            message += "\n" + e.getClass().getName() + ":\n" + e.getMessage();
        }
        JOptionPane.showMessageDialog(Main.instance, message, "Error", JOptionPane.ERROR_MESSAGE);
    }
}
